//
//  WeatherAPIService.swift
//  SwiftWeather
//
//  Created by shen on 2023/8/28.
//

import Foundation
import CoreLocation
import Combine

final class WeatherAPIService {
    enum URLError: LocalizedError {
        case invalidURL

        var errorDescription: String? {
            switch self {
            case .invalidURL:
                return "Invalid address of the requested resource"
            }
        }
    }

    // MARK: Properties

    static let shared: WeatherAPIService = .init()

    private let session = URLSession.shared
    private let decoder: JSONDecoder = .init()
    private let service: URLService = .init()
    private var accessKey: String

    // MARK: - Initialization

    private init() {
        guard let key = AccessKeysHelper.getKey(type: .weatherApiKey) else {
            fatalError("You must have an API key to access the data")
        }
        self.accessKey = key
    }

    // MARK: - Methods

    func getForecast(for location: CLLocationCoordinate2D) throws -> AnyPublisher<Weather, HTTPError> {
        try getForecast(coordinate: (location.latitude, location.longitude))
    }

    func getForecast(coordinate: Coordinate) throws -> AnyPublisher<Weather, HTTPError> {
        guard let url = service.createURL(
            scheme: WeatherHTTPBase.scheme,
            host: WeatherHTTPBase.host,
            path: WeatherAPIRoute.forecast.path,
            queryParameters: prepareForecastQueryParameters(coordinate: coordinate, days: 3)
        ) else {
            throw URLError.invalidURL
        }

        return createNetworkCallPublisher(withOutputType: Weather.self, url: url)
    }

    func searchCity(query: String) throws -> AnyPublisher<[City], HTTPError> {
        guard let url = service.createURL(
            scheme: WeatherHTTPBase.scheme,
            host: WeatherHTTPBase.host,
            path: WeatherAPIRoute.search.path,
            queryParameters: prepareSearchQueryParameters(query: query)
        ) else {
            throw URLError.invalidURL
        }

        return createNetworkCallPublisher(withOutputType: [City].self, url: url)
    }
}

// MARK: - Private methods

private extension WeatherAPIService {
    func createNetworkCallPublisher<T: Decodable>(withOutputType outputType: T.Type, url: URL) -> AnyPublisher<T, HTTPError> {
        session.dataTaskPublisher(for: url)
            .assumeHTTP()
            .map { $0.data }
            .decode(type: outputType.self, decoder: decoder)
            .mapHTTPError()
            .eraseToAnyPublisher()
    }

    func prepareForecastQueryParameters(coordinate: Coordinate, days: Int) -> [String: String] {
        return [
            "q": "\(coordinate.latitude) \(coordinate.longitude)",
            "key": accessKey,
            "days": String(days),
            "aqi": "yes",
        ]
    }

    func prepareSearchQueryParameters(query: String) -> [String: String] {
        return [
            "q": query,
            "key": accessKey,
        ]
    }
}
